/*
 * \file BaseAbilitiesWidget.cpp
 *
 * \date Created on: Jan 21, 2009
 * \author sarah
 */

#include "BaseAbilitiesWidget.h"

#include <DndData/DndData.h>
#include <DndData/CharacterData.h>

#include <QTimer>
#include <QMessageBox>
#include <QList>
#include <QtDebug>

#include <algorithm>

BaseAbilitiesWidget::BaseAbilitiesWidget(CharacterData *data, QWidget *parent)
	: QWidget(parent)
	, mAbilityArraySliders(new QSlider*[DndData::NUM_ABILITIES])
        , mCharacterData(data)
        , mSlidersModified(false)
	, mSlidersModifiedWarningGiven(false)
	, mCustomPoints( 0 )
	, mLowAbilityWarningGiven(false)
	, mRerollCount(0)
{
	mPage.setupUi(this);
	mAbilityArraySliders[0] = mPage.strengthSlider;
	mAbilityArraySliders[1] = mPage.constitutionSlider;
	mAbilityArraySliders[2] = mPage.dexteritySlider;
	mAbilityArraySliders[3] = mPage.intelligenceSlider;
	mAbilityArraySliders[4] = mPage.wisdomSlider;
	mAbilityArraySliders[5] = mPage.charismaSlider;
	QTimer::singleShot(0, this, SLOT(initialize()));
}

BaseAbilitiesWidget::~BaseAbilitiesWidget()
{
	delete[] mAbilityArraySliders;
}

void BaseAbilitiesWidget::initialize()
{
	// set the combos and corresponding character slots to initial values
	for (int sliderNum = 0; sliderNum < DndData::NUM_ABILITIES; ++sliderNum)
	{
		QSlider *slider = mAbilityArraySliders[sliderNum];
		connect(slider, SIGNAL(sliderReleased()), this, SLOT(abilitySliderChanged()));
	}
	setAbilityArray(DndData::STANDARD_ARRAY);
	mPage.totalModifiersWarningIconLabel->setVisible(false);
	mPage.totalModifiersWarningTextLabel->setVisible(false);
	mPage.rerollCountLcdNumber->display(mRerollCount);
}

/**
 * Handler for class combo box changes.
 *
 * Sets the ability sliders to the values optimal for the given class.
 */
void BaseAbilitiesWidget::updateToClass(int index)
{
	int choice = index - 1;
	const int *optimumScores = DndData::APTITUDE_ABILITIES[choice];
	if (!checkChangesOk(DndData::CLASS_NAMES[choice]))
		return;
	int scoreArray[DndData::NUM_ABILITIES];
	std::sort(mAbilityArray, mAbilityArray + DndData::NUM_ABILITIES, std::greater<int>());
	for (int rank = 0; rank < DndData::NUM_ABILITIES; ++rank)
	{
		int ability = optimumScores[rank];
		Q_ASSERT(rank < DndData::NUM_ABILITIES);
		scoreArray[ability] = mAbilityArray[rank];
	}
	setAbilityArray(scoreArray);
}


void BaseAbilitiesWidget::setAbilityArray(const int abilityArray[])
{
	// set the sliders and corresponding character slots to initial values
	for (int sliderNum = 0; sliderNum < DndData::NUM_ABILITIES; ++sliderNum)
	{
		int value = abilityArray[sliderNum];
		mAbilityArray[sliderNum] = value;
                mCharacterData->mAbilities[sliderNum] = value;
		QSlider *slider = mAbilityArraySliders[sliderNum];
		slider->setValue(value);
		mPreviousIndexes[slider] = value;
	}
	mSlidersModified = false;
}

/**
 * Generate a random array of ability rolls, in line with the D&D Players
 * handbook section in Chapter 2 "Generating Ability Scores", sub-section
 * "Method 3: Rolling Scores".
 * @return int * static array, DndData::NUM_ABILITIES in length
 */
int *BaseAbilitiesWidget::getRandomArray()
{
	static int randomArray[DndData::NUM_ABILITIES] = { 0 };
	for (int abil = 0; abil < DndData::NUM_ABILITIES; ++abil)
	{
		int min = 6;
		int total = 0;
		for (int die = 0; die < 4; ++die)
		{
			int value = 1 + (rand() % 6);
			if (value < min)
			{
				min = value;
			}
			total += value;
		}
		total -= min;
		randomArray[abil] = total;
	}
	return randomArray;
}


/**
 * Display a warning and return the a response if its OK to use
 * slider values where the total of modifiers is < 4 or > 8.
 * @returns true if its OK to use, previousChoice otherwise.
 */
void BaseAbilitiesWidget::modifierTotalUpdate()
{
	int total = 0;
	for (int ability = 0; ability < DndData::NUM_ABILITIES; ++ability)
		total += DndData::abilityModifier(mAbilityArray[ability]);
	mPage.totalOfModifiersLcdNumber->display(total);
	mPage.totalModifiersWarningIconLabel->setVisible(total < 4 || total > 8);
	mPage.totalModifiersWarningTextLabel->setVisible(total < 4 || total > 8);
	mPage.totalModifiersWarningTextLabel->setText(
					total < 4 ? tr("Too low!") : tr("Too high!"));
}

void BaseAbilitiesWidget::on_abilitiesTabWidget_currentChanged(int index)
{
	switch (index)
	{
		case StandardArrayTab:
			setAbilityArray(DndData::STANDARD_ARRAY);
			break;
		case CustomizeScoresTab:
			mCustomPoints = DndData::CUSTOMIZED_POINTS;
			setAbilityArray(DndData::CUSTOMIZE_SCORES_ARRAY);
			mPage.availablePointsLcdNumber->display(mCustomPoints);
			break;
		case RollDiceTab:
			updateDiceRoll();
			break;
	}

}

void BaseAbilitiesWidget::on_rerollPushButton_clicked(bool)
{
	updateDiceRoll();
}

void BaseAbilitiesWidget::updateDiceRoll()
{
	setAbilityArray(getRandomArray());
	int total = 0;
	for (int ability = 0; ability < DndData::NUM_ABILITIES; ++ability)
		total += DndData::abilityModifier(mAbilityArray[ability]);
	modifierTotalUpdate();
	++mRerollCount;
	mPage.rerollCountLcdNumber->display(mRerollCount);
}

/**
 * Display a warning and return the a response if its OK to overwrite
 * slider values with the optimum values for a chosen class.
 * @param className chosen.
 * @returns true if its OK to overwrite, false otherwise.
 * Note that if the sliders have not been modified, or if the warning has
 * already been given then no warning is given and true is returned.
 */
bool BaseAbilitiesWidget::checkChangesOk(const QString &className) {
	if (mSlidersModified && !mSlidersModifiedWarningGiven)
	{
		QMessageBox msgBox;
		msgBox.setIcon(QMessageBox::Question);
		msgBox.setText(tr("Abilities have been modified."));
		msgBox.setInformativeText(tr("Overwrite with optimum values for %1?").arg(
				className));
		msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
		msgBox.setDefaultButton(QMessageBox::Ok);
		int ret = msgBox.exec();
		mSlidersModifiedWarningGiven = true;
		return (ret == QMessageBox::Ok);
	}
	return true;
}

/**
 * Find the closest value in the given array to the given choice.
 * @param slider that may change when adjusted to closest.
 * @param int choice the value selected.
 * @return int the closest value.
 */
int BaseAbilitiesWidget::setToClosest(int sliderIndex, int choice)
{
	int minDist = 100;
	int closestChoice = -1;
	for (int abil = 0; abil < DndData::NUM_ABILITIES; ++abil)
	{
		int abilValue = mAbilityArray[abil];
		if (abs(abilValue - choice) < minDist) {
			minDist = abs(abilValue - choice);
			closestChoice = abilValue;
		}
	}
	Q_ASSERT(closestChoice != -1);
	if (closestChoice != choice)
		mAbilityArraySliders[sliderIndex]->setValue(closestChoice);
	return closestChoice;
}

/**
 * The active slider changed, so exactly one of the others now has an
 * invalid value.  That invalid combo must be set to the previous value,
 * effectively swapping the two slider's values.  Note that the first
 * matching slider value is chosen.
 * @param activeSlider is the slider that changed.
 * @param int choice is the value the slider changed to.
 * @param int previousChoice is the value the slider changed from.
 */
void BaseAbilitiesWidget::redistributeValues(int sliderIndex, int choice, int previousChoice)
{
	QSlider *mustChange = NULL;
	for (int slider = 0; slider < DndData::NUM_ABILITIES; ++slider) {
		if (slider == sliderIndex)
			continue;
		if (mAbilityArraySliders[slider]->value() == choice) {
			mustChange = mAbilityArraySliders[slider];
			break;
		}
	}
	Q_ASSERT(mustChange);
	mustChange->setValue(previousChoice);
	mPreviousIndexes[mustChange] = previousChoice;
}

/**
 * Maintain the invariant that there is a maximum of one slider set to
 * a value < 10.  Called when a slider is set down to 8 or 9.
 * @param activeSlider the slider that was moved.
 * @returns int the number of points restored by restoring the invariant.
 * Note if there was no other slider set low, then no action is done, and
 * 0 is returned.
 */
int BaseAbilitiesWidget::checkLowSlider(QSlider *activeSlider)
{
	QSlider *lowSlider = NULL;
	int scoreAdjust = 0;
	for (int slider = 0; slider < DndData::NUM_ABILITIES; ++slider) {
		if (activeSlider == mAbilityArraySliders[slider])
			continue;
		if (mAbilityArraySliders[slider]->value() < 10) {
			Q_ASSERT(lowSlider == NULL);
			lowSlider = mAbilityArraySliders[slider];
		}
	}
	if (lowSlider)
	{
		scoreAdjust = lowSlider->value() == 8 ? 2 : 1;
		lowSlider->setValue(10);
	}
	return scoreAdjust;
}

/**
 * Maintain the costs of slider adjustments, correcting down if there are
 * insufficient points available to make the requested adjustment.
 * @param sliderIndex is the index of the slider that changed.
 * @param int choice is the value the slider changed to.
 * @param int previousChoice is the value the slider changed from.
 * @return int the possibly adjusted choice.
 */
int BaseAbilitiesWidget::maintainCustomizePoints(int sliderIndex, int choice, int previousChoice)
{
	Q_ASSERT(previousChoice >= 8 && previousChoice <= 18);
	Q_ASSERT(choice >= 8 && choice <= 18);
	if (choice < 10)
		mCustomPoints += checkLowSlider(mAbilityArraySliders[sliderIndex]);
	int prevCost = DndData::CUSTOMIZED_COSTS[previousChoice - 8];
	int cost = 0;
	while (true)
	{
		cost = DndData::CUSTOMIZED_COSTS[choice - 8];
		if (cost - prevCost <= mCustomPoints)
			break;
		--choice;
		Q_ASSERT(choice >= 8);
	}
	mAbilityArraySliders[sliderIndex]->setValue(choice);
	mAbilityArray[sliderIndex] = choice;
	mCustomPoints -= (cost - prevCost);
	mPage.availablePointsLcdNumber->display(mCustomPoints);
	return choice;
}

/**
 * Clip the value given to the range 8 to 18 inclusive.
 * @param slider which may be clipped.
 * @param int choice the value to clip.
 * @returns the value, ensured to be 8 to 18 inclusive.
 */
int BaseAbilitiesWidget::clipToRange(int sliderIndex, int choice)
{
	int adjustedChoice = choice < 8 ? 8 : (choice > 18 ? 18 : choice);
	if (choice != adjustedChoice)
	{
		mAbilityArraySliders[sliderIndex]->setValue(adjustedChoice);
	}
	return adjustedChoice;
}

/**
 * Handler for ability slider mouse-up.  When slider is released, checks and
 * maintains invariants for each type of ability score model: standard
 * array, customized or dice roll.  Sliders are clipped/modified as required,
 * and if needed model parameters are updated.
 */
void BaseAbilitiesWidget::abilitySliderChanged()
{
	QSlider *activeSlider = dynamic_cast<QSlider*>(sender());
	std::map<QSlider*,int>::iterator activeSliderPrev = mPreviousIndexes.find(activeSlider);
	Q_ASSERT(activeSliderPrev != mPreviousIndexes.end());
	int choice = activeSlider->value();
	if (choice == activeSliderPrev->second) {  // not changed
		return;
	}
	int previousChoice = activeSliderPrev->second;
	QSlider **iter = std::find(mAbilityArraySliders,
			mAbilityArraySliders + DndData::NUM_ABILITIES, activeSlider);
	int sliderIndex = iter - mAbilityArraySliders;
	switch (mPage.abilitiesTabWidget->currentIndex())
	{
		case CustomizeScoresTab:
			choice = clipToRange(sliderIndex, choice);
			if (choice == previousChoice) return; // unchanged after clipping
			choice = maintainCustomizePoints(sliderIndex, choice, previousChoice);
			if (choice == previousChoice) return; // unchanged due to out of points
			break;
		case StandardArrayTab:
		case RollDiceTab:
			choice = setToClosest(sliderIndex, choice);
			if (choice == previousChoice) return; // unchanged after set to closest
			redistributeValues(sliderIndex, choice, previousChoice);
			break;
	}
	mPreviousIndexes[activeSlider] = choice;
	mSlidersModified = true;
        mCharacterData->mAbilities[sliderIndex] = choice;
}
